Skip to content

Add cross-cutting Watermark, Stamp, Rotation, and Split options#73

Merged
Jaben merged 4 commits intodevelopfrom
feature/cross-cutting-options
Mar 29, 2026
Merged

Add cross-cutting Watermark, Stamp, Rotation, and Split options#73
Jaben merged 4 commits intodevelopfrom
feature/cross-cutting-options

Conversation

@Jaben
Copy link
Copy Markdown
Member

@Jaben Jaben commented Mar 28, 2026

Summary

  • Introduce four new cross-cutting facets on BuildRequestBase (available on all request types): RotationOptions, SplitOptions, WatermarkOptions, StampOptions
  • Add DDD value objects: RotationAngle (90/180/270), PageRanges (regex-validated "1-3,5"), OverlaySource enum (text/image/pdf), SplitMode enum (intervals/pages)
  • Wire into BaseBuilder with SetRotationOptions, SetSplitOptions, SetWatermarkOptions, SetStampOptions fluent methods
  • Convenience methods: SetTextWatermark, SetTextStamp, SplitByIntervals, SplitByPages

Test plan

  • 27 unit tests covering value objects, builders, and serialization
  • 2 integration tests (rotation + watermark) against local Gotenberg 8
  • WatermarkAndRotate example console app added
  • README sections for watermark/rotation and split added

Summary by CodeRabbit

  • New Features

    • Added watermarking capability with text watermarks and page range selection
    • Added page rotation with configurable angles
    • Added PDF splitting by page ranges or intervals with unification options
    • Added stamping for text, image, or PDF overlays
    • Enhanced builder API for easy configuration of these new options
  • Documentation

    • Added examples demonstrating watermarking and rotation features
    • Updated documentation with split and advanced feature examples
  • Tests

    • Added comprehensive test coverage for all new PDF manipulation features

Jaben added 2 commits March 28, 2026 12:05
Introduce four new cross-cutting facets on BuildRequestBase, available
on all request types: RotationOptions, SplitOptions, WatermarkOptions,
and StampOptions.

Add DDD value objects: RotationAngle (90/180/270), PageRanges (validated
format like "1-3,5"), OverlaySource enum (text/image/pdf), and SplitMode
enum (intervals/pages). All enforce domain constraints at creation time.

Wire into BaseBuilder with SetRotationOptions, SetSplitOptions,
SetWatermarkOptions, and SetStampOptions fluent builder methods.
Add console example demonstrating watermark and rotation options.
Add README sections for watermark/rotation and split features.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32f5253e-0ba1-4be8-8054-c5a7aac3349c

📥 Commits

Reviewing files that changed from the base of the PR and between 4872a12 and d058cb2.

📒 Files selected for processing (5)
  • README.md
  • examples/WatermarkAndRotate/Program.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs
  • src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
  • test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs

📝 Walkthrough

Walkthrough

This pull request introduces fluent builder methods and request facet classes for four cross-cutting PDF modification features: rotation, split, watermark, and stamp. New builder classes (RotationOptionsBuilder, SplitOptionsBuilder, WatermarkOptionsBuilder, StampOptionsBuilder) enable request configuration via lambda expressions on BaseBuilder. Request base class extended to include these options for HTTP serialization. New value object and constants support form-data conversion.

Changes

Cohort / File(s) Summary
Documentation & Examples
README.md, examples/WatermarkAndRotate/*
Updated README with new "Advanced Features" examples for watermark/rotation and PDF splitting. Added new example program demonstrating fluent builder configuration for watermarked, rotated PDFs with inline HTML and timestamped file output.
Builder & Options Builder Classes
src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseBuilder.cs, src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/RotationOptionsBuilder.cs, src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/SplitOptionsBuilder.cs, src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/WatermarkOptionsBuilder.cs, src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/StampOptionsBuilder.cs
Added four fluent setter methods to BaseBuilder accepting lambda-configured options builders. Introduced four new sealed builder classes with null-validation, lazy instantiation, and convenience methods (e.g., SplitByPages, SetTextWatermark, SetTextStamp).
Request Facet Models
src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs, src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/RotationOptions.cs, src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/SplitOptions.cs, src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/WatermarkOptions.cs, src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/StampOptions.cs
Extended BuildRequestBase with four nullable option properties and serialization support. Added four new facet classes with MultiFormHeader annotations mapping properties to cross-cutting request header constants.
Value Objects & Serialization
src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/OverlaySource.cs, src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs, src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
Added OverlaySource enum (Text, Image, Pdf) with ToFormValue extension. Extended FacetBase.GetValueAsInvariantCultureString to handle OverlaySource and SplitMode conversion. Added CrossCuttingOptions constant class with 14 form-header keys for rotation, split, watermark, and stamp options.
Tests
test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs
Comprehensive test suite covering value-object validation (RotationAngle.Create, PageRanges.Create), builder option application, HTTP serialization with form-data assertion, and integration tests with authenticated client for rotation and watermark workflows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

  • PR #63: Extends facet and constant infrastructure that this PR directly builds upon for rotation/split/watermark/stamp models and cross-cutting option keys.
  • PR #65: Modifies builder API surface and builder class structures, sharing similar fluent builder pattern additions in this PR.
  • PR #61: Extends FacetBase value-to-form conversion to support new value types; this PR adds similar conversions for OverlaySource and SplitMode.

Poem

🐰 Builders bloom with fluent grace,
Rotation, stamps, and splits embrace,
Watermarks now dance with ease,
Cross-cutting options built to please!
PDFs transform at builder's call,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding four cross-cutting options (Watermark, Stamp, Rotation, Split) to the API client library.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/cross-cutting-options

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
examples/WatermarkAndRotate/Program.cs (2)

53-58: Consider checking response before writing to file.

If HtmlToPdfAsync returns null or the request fails, CopyToAsync will throw. Adding a null check or try-catch would make the example more robust for users copying this pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/WatermarkAndRotate/Program.cs` around lines 53 - 58, The code
assumes sharpClient.HtmlToPdfAsync(request) always returns a non-null stream;
add a null-check and error handling before calling response.CopyToAsync: verify
the returned response from HtmlToPdfAsync is not null, and wrap the file write
(using resultPath and destinationStream) in a try-catch to log or rethrow a
meaningful error if the request failed or the stream is null; ensure you
dispose/close response and destinationStream properly in the error path.

25-34: Potential double-disposal of HttpClientHandler when using BasicAuthHandler.

When authHandler is created (lines 26-28), it takes ownership of handler via InnerHandler = handler. When authHandler is disposed, it will dispose handler as its inner handler. The separate using var handler declaration will then attempt to dispose an already-disposed object.

While HttpClientHandler.Dispose() is typically idempotent, the pattern is fragile. Consider restructuring to avoid the double-dispose:

Suggested fix
-    using var handler = new HttpClientHandler();
-    using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword)
-        ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler }
-        : null;
-
-    using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler)
+    var handler = new HttpClientHandler();
+    HttpMessageHandler effectiveHandler = handler;
+    
+    if (!string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword))
+    {
+        effectiveHandler = new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler };
+    }
+
+    using var httpClient = new HttpClient(effectiveHandler, disposeHandler: true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/WatermarkAndRotate/Program.cs` around lines 25 - 34, The current
pattern double-disposes HttpClientHandler because handler is declared with
"using var handler" and also assigned as InnerHandler of BasicAuthHandler
(authHandler) which will dispose it; fix by only disposing the outermost
handler: remove the separate using on HttpClientHandler and instead create
handler as a plain variable and wrap only the final top-level
HttpMessageHandler/HttpClient in a using; alternatively, instantiate either
BasicAuthHandler or the raw HttpClientHandler inside a single using block so
only the top-level handler (authHandler or handler) is disposed when
constructing HttpClient (refer to HttpClientHandler, BasicAuthHandler,
authHandler, handler, and HttpClient).
test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs (2)

166-171: Consider using async/await instead of .Result.

Using .Result on async methods can cause deadlocks in certain synchronization contexts. While this is generally safe in unit tests without a synchronization context, the idiomatic approach would be to make the test method async.

♻️ Suggested refactor to use async pattern
 [Test]
-public void RotationOptions_SerializesCorrectly()
+public async Task RotationOptions_SerializesCorrectly()
 {
     var options = new RotationOptions
     {
         RotateAngle = RotationAngle.Degrees90,
         RotatePages = PageRanges.Create("1-3")
     };

     var httpContents = options.ToHttpContent().ToList();

-    httpContents.FirstOrDefault(c =>
-        c.Headers.ContentDisposition?.Name == "rotateAngle")!
-        .ReadAsStringAsync().Result.Should().Be("90");
-    httpContents.FirstOrDefault(c =>
-        c.Headers.ContentDisposition?.Name == "rotatePages")!
-        .ReadAsStringAsync().Result.Should().Be("1-3");
+    var angleContent = httpContents.FirstOrDefault(c =>
+        c.Headers.ContentDisposition?.Name == "rotateAngle")!;
+    (await angleContent.ReadAsStringAsync()).Should().Be("90");
+    
+    var pagesContent = httpContents.FirstOrDefault(c =>
+        c.Headers.ContentDisposition?.Name == "rotatePages")!;
+    (await pagesContent.ReadAsStringAsync()).Should().Be("1-3");
 }

Apply similar changes to other serialization tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs` around lines 166
- 171, The test currently blocks on async reads using
ReadAsStringAsync().Result; change the test method (e.g., in
CrossCuttingOptionsTests) to async Task and replace .Result calls with await
ReadAsStringAsync() for the two assertions that inspect httpContents entries
with ContentDisposition.Name "rotateAngle" and "rotatePages"; update the
assertions to await the string results before calling Should().Be("90") and
Should().Be("1-3"), and apply the same async/await pattern to other
serialization tests that use ReadAsStringAsync().Result.

240-254: Consider marking integration tests with a category attribute.

These tests require a running Gotenberg instance at localhost:3000 and will fail in CI environments without one. Adding a [Category("Integration")] or [Explicit] attribute would allow these to be excluded from standard unit test runs.

♻️ Suggested addition of category attribute
+[Category("Integration")]
 [Test]
 public async Task HtmlToPdf_WithRotation_Succeeds()
 {

Apply similar changes to HtmlToPdf_WithWatermark_Succeeds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs` around lines 240
- 254, The two integration tests HtmlToPdf_WithRotation_Succeeds and
HtmlToPdf_WithWatermark_Succeeds are currently runnable in CI and require a
Gotenberg instance; mark them to be excluded from standard unit runs by adding a
test attribute such as [Category("Integration")] or [Explicit] above each NUnit
[Test] declaration (i.e., annotate the HtmlToPdf_WithRotation_Succeeds and
HtmlToPdf_WithWatermark_Succeeds methods) so test runners can filter or skip
them when integration services are unavailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/WatermarkAndRotate/Program.cs`:
- Around line 53-58: The code assumes sharpClient.HtmlToPdfAsync(request) always
returns a non-null stream; add a null-check and error handling before calling
response.CopyToAsync: verify the returned response from HtmlToPdfAsync is not
null, and wrap the file write (using resultPath and destinationStream) in a
try-catch to log or rethrow a meaningful error if the request failed or the
stream is null; ensure you dispose/close response and destinationStream properly
in the error path.
- Around line 25-34: The current pattern double-disposes HttpClientHandler
because handler is declared with "using var handler" and also assigned as
InnerHandler of BasicAuthHandler (authHandler) which will dispose it; fix by
only disposing the outermost handler: remove the separate using on
HttpClientHandler and instead create handler as a plain variable and wrap only
the final top-level HttpMessageHandler/HttpClient in a using; alternatively,
instantiate either BasicAuthHandler or the raw HttpClientHandler inside a single
using block so only the top-level handler (authHandler or handler) is disposed
when constructing HttpClient (refer to HttpClientHandler, BasicAuthHandler,
authHandler, handler, and HttpClient).

In `@test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs`:
- Around line 166-171: The test currently blocks on async reads using
ReadAsStringAsync().Result; change the test method (e.g., in
CrossCuttingOptionsTests) to async Task and replace .Result calls with await
ReadAsStringAsync() for the two assertions that inspect httpContents entries
with ContentDisposition.Name "rotateAngle" and "rotatePages"; update the
assertions to await the string results before calling Should().Be("90") and
Should().Be("1-3"), and apply the same async/await pattern to other
serialization tests that use ReadAsStringAsync().Result.
- Around line 240-254: The two integration tests HtmlToPdf_WithRotation_Succeeds
and HtmlToPdf_WithWatermark_Succeeds are currently runnable in CI and require a
Gotenberg instance; mark them to be excluded from standard unit runs by adding a
test attribute such as [Category("Integration")] or [Explicit] above each NUnit
[Test] declaration (i.e., annotate the HtmlToPdf_WithRotation_Succeeds and
HtmlToPdf_WithWatermark_Succeeds methods) so test runners can filter or skip
them when integration services are unavailable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3f97fb1-d3ea-4b8c-ba6c-3d4380311f46

📥 Commits

Reviewing files that changed from the base of the PR and between 09b6c02 and 4872a12.

📒 Files selected for processing (20)
  • README.md
  • examples/WatermarkAndRotate/Program.cs
  • examples/WatermarkAndRotate/WatermarkAndRotate.csproj
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/RotationOptionsBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/SplitOptionsBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/StampOptionsBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/WatermarkOptionsBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/RotationOptions.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/SplitOptions.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/StampOptions.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/WatermarkOptions.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/OverlaySource.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/PageRanges.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/RotationAngle.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/SplitMode.cs
  • src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
  • test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs

Jaben added 2 commits March 28, 2026 22:14
- Fix double-dispose of HttpClientHandler in example (use disposeHandler: true)
- Switch serialization tests from .Result to async/await
- Add [Category("Integration")] to integration tests for CI filtering
Resolve conflicts in README.md (keep both cross-cutting and standalone
PDF sections) and PageRanges.cs (keep semantic validation from develop).
@Jaben Jaben merged commit 3f225a9 into develop Mar 29, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant